翻译|React & Redux Tutorial — Build a Hacker News Clone

翻译|React & Redux Tutorial — Build a Hacker News Clone

– Build a production React project using Redux and Styled Components. Deploy the app using GitHub pages.

原文参见

本文是 gitconnected Hacktobrefest项目的逐步解决方法.

在本教程中,我将会构建一个产品级别的的 Hacker News 克隆. 我们会逐步实现应用的初始化,添加用于状态管理的 Redux,用 React 构建 UI并且部署到 GitHub 主页上.样式将会采用`styled-components,API方面使用axios库调用 Hacker News API.

源代码在这里查看.

下载 Chrome应用

如果你愿意看视频,可以看看 youtube 上的教程. http://www.youtube.com/watch?v=oGB_VPrld0U&index=2&list=PLTTC1K14KAxHj6AftnRUD28SQaoVauvl3

初始化项目

使用create-react-app来初始化项目.用这个包初始化项目,就不用担心配置问题了.首先要确定已经安装了create-react-app.

1
npm -i -g create-react-app

运行下面的命令来启动项目. create-react-app 安装了所有构建 React 应用的必备依赖包,还有默认的脚本用于管理开发和实际应用的打包.

1
2
3
4
5
create-react-app hn-clone

# wait for everything to finish...

cd hn-clone

现在可以安装应用所需的核心软件包了.目前我使用的是yarn来管理依赖包,如果你使用的是npm,只需要用npm install替换掉yarn add就可以了.

1
yarn add redux styled-components react-redux redux-logger redux-thunk axios

create-react-app使用NODE_PATH 环境变量(environment variable)来创建绝对路径. 我们可以在.env文件中声明环境变量. create-react-app会识别它,通过doten库 来应用绝对路径.

1
2
3
4
5
#使用touch 命令创建.env文件

touch .env
# 在.env文件里添加
#NODE_PATH=src

如果你对这个模式不太熟悉, 当我们开始构建应用的时候,对你来说更为有意义.设定环境变量可以让我们直接导入文件而不用考虑文件的路径. 类似这样 ../../components/List 变为components/List- 使用上方便多了.

文件组织结构

src文件夹里面, 从应用要适应更为大规模和重用性更强上考虑,做一些更新.

  • components: 这个文件夹包含所有的 React 组件(container和 presentational 组件都包含).
  • services: Services可以连接到API(例如,使用axios调用 HN API)或者为应用提供扩展的功能(例如,添加markdown)支持.
  • store: store 包含了所有的Redux和state 管理的逻辑
  • styles: 在styles文件夹内,我们声明变量,模板和可以在组件间共享的样式模式
  • utils: 整个应用中可以重用的助手函数

这里的文件夹结构有两个地方值得注意:

  1. 应用中只有一个路由,位于根./下.如果我们有多个路由,我可能会使用react-router包,同时创建pages文件夹用于保存页面级别的组件.
  2. 我没有使用单独的container文件夹用于连接应用组件到Redux.我发现增加container文件夹反而添加了不必要的复杂性,让一些新手感到很困惑,因为开发者总是要从没有关联的位置中导入文件(container想要连接组件,反之亦然). 在我的使用经验汇总,从当个来源导入文件工作的更好一点.

因为我们在使用styled-components,所以可以删除掉index.cssapp.css文件. 现在我们要在src/styles文件件中添加一些基础模板样式,创建文件global.jspalette.js文件

Palette包含了应用UI中使用的成组的颜色配置. 在src/styles/palette.js中添加

global.js用于生成应用中共享的基础样式. styled-componentsinjectGlobal方法应该要小心使用,但是用于应用级别的样式时时非常有用的.

注意: 在styled-components v4中injectGlobal已经被createGlobalStyle替代了.

components文件夹中创建App文件夹,把所有的 CRA默认生成的文件都移动到这个文件中,把App.js文件重命名为index.js文件. 这样就可以导入components/App

现在代开src/index.js文件(项目的根文件),使用更新的文件结构更新文件.

注意,因为之前我们定义了NODE_PATH,现在使用components/App导入App文件,styles/globals来导入setGlobalStyles文件. 执行setGlobalStyles()函数可以在应用中导入全局的样式.

现在我们已经准备好了启动应用开发环境的核心配置. 运行下面命令启动应用,会在http://localhost:3000看到应用. 现在看上去还不是太好,但是应用已经跑起来了 :)

1
2
yarn start
# npm 安装用 npm start

在 React 应用添加Redux

src/store文件中,创建index.js,reducer.jsmiddleware.js文件. 让我们来初始化一个app专项(feature)来管理应用的state.

以我的经验,在生产级别的应用中,如果按照特性而不是按照功能进行分组,Redux会更具有管理性,类似于鸭子方法(Ducks approach). “按照功能分组(grouping by functionality)” 方法中所有的actions,reducers,等等都位于独立的文件夹中, 当应用规模增加时,在不同文件中切换难度就增大了. 如果按照特性分组,你需要的文件总是在一个位置.

index.js文件中,创建configureStore函数,用于初始化应有的 Redux.

使用createStore构建初始化store. 从根reducer文件导入reducer,同时从middleware配置文件中导入 middleware(中间件). initialState 应该在程序运行时提供,并传递给我们的函数. 在生产中,要能够管理复杂的功能例如 SSR(服务端渲染),或者在初始化时从服务器获取传递的数据. 在这里初始state,可以让我们更优雅的和抽象出store的创建过程.

reducer.js文件中,使用combineReducers函数创建根reducer.此函数把所有的reducer函数组合起来生成单个的state树.

接下来在middleware.js中创建中间件. 中间件是每一次dispatch action 时都必须要执行的函数. 中间在扩展Redux应用时非常有用. 在文件中添加如下代码

也要构建第一个Reducer.在 src/store/app文件加中创建 reducer.jsaction.js文件. 需要添加日间/夜间的切换模式功能,所以让我们创建一个action来管理这个特性.在src/store/app/action.js 添加下面代码

我们创建了一个actionTypes对象放置actio-type常量. 类似的常量在reducer中用于匹配改变state的类型. 也要创建actions对象,包含了可以从应用中dispatch 用于改变state的 action函数.每一个action都包括了一个type和一个payload(译注: type告诉store要干什么,payload 是执行action时携带的条件).

最后,创建我们的reducer

当我们dispatch一个SET_THEME action时, 将会使用payload的内容更新 state中theme的属性值. payload是一个对象,形式是{theme:'value'}.使用es6的展开操作...,state中对应payload键的值会被替换掉.

如果需要详细理解 Redux的基础 ,看看Dan Abramov的视频

现在返回src/index.js文件,做一些更新,需要把我们的应用连接到 Redux. 为Provider添加一个导入,更新渲染方法

现在应该已经做完了 Redux的整个工作.返回到localhost:3000,在Chrome的console中可以看到下面的内容

使用 React和Styld Components 构建 UI

现在 Redux 已经初始化完毕, 开始完成 UI 的工作. 首先声明一些会在应用中使用的样式常量. 在本应用中,我们要创建mediaQueries(媒体查询) 文件包含构建响应式应用的常量. 创建src/styles/mediaQueries.js文件,添加下面的代码

返回到src/components/App文件夹, 在index.js文件中,更新文件内容

其中使用了styled-componentsThemeProvider组件.这个组件尅让我们把”theme”作为prop传递给创建的styled components. 这里初始化theme为 colorDark对象.

App中包含的组件,现在还没有创建,所以现在来创建.首先构建styld-components 组件. 在App文件夹里创建styles.js文件, 添加代码

创建的用于页面的div称为Wrapper. 用于页面标题的h1创建为Title组件. styled-components语法使用styled对象定义 HTML 元素. 可以用字符串定义组件的 CSS 属性.

注意代码20行, 我们使用了theme prop. 包含props参数的函数由styled-components 注入到样式字符串中,这么,我们就可提起属性或者添加用于动态构建样式的逻辑,从组件中抽象出构建样式的逻辑.

接下来, 创建包含 Hacker Nees故事的 List 组件. 创建src/components/List文件夹并添加index.js,styles.js文件. 在index.js文件中,添加代码

styles.js文件中创建ListWrapper.使用从ThemeProvider组件得到的theme props 的background-color属性.

最后创建ListItem组件用于显示单个的故事. 创建src/components/ListItem文件夹和index.js,styles.js文件.

我们想让 UI模仿 Hacker News. 目前会在ListItem中使用fake 数据里模拟. 在index.js文件中添加代码

每个故事都有标题,作者,评分,发帖时间,URL地址,评论数. 初始化这几个值,以便于查看 UI 的样子. 基于安全原因, 添加rel="nofollow noreferrer noopener".

styles.js文件中添加下面代码

这些应该就是我们需要的基础 UI 组件了. 返回到浏览器,应该看到使用fake数据的单个条目

使用 Redux 和 Axios 构建 API 调用

是时候在应用添加实际数据了.我们通过axios库来调用 Hacker News的 API.调用 API 的过程会在应用中引入 “side effect(副作用)”,意思是调用 API 会从外部资源影响本地环境的state.

API 调用之所以被称为 side effect,原因是在应用的state中引入了外部的数据. 其他的side effect的例子包括和浏览器的localStorage的交互操作, 追踪用户分析,连接到web socket,等等. 在 Redux 应用中可以使用很多库来管理 side effect. 从简单的redux-thunk 到更为复杂的redux-saga. 然而他们的目的是相同的,就是让 Redux与外界交互. redux-thunk是最简单的库, 可以在action 对象中再次 dispatch JavaScript 函数.
这个功能就是我们在使用axios时需要的功能,在 API调用管理返回的promise对象.

src/services文件夹中,创建Api.jshackerNewsApi.js文件. axios库有着难以置信的强大功能和扩展性. Api.js包含的配置使得执行axios请求更容易. 这里没有拷贝完整代码,你可以在源代码中看到信息内容,其中包含了更为精细的配置.

src/services/hackerNewsApi.js文件中, 我们要定义请求 Hacker News API 的函数. 在Hacker New API 文档 可以找到,如果要获取 IDs 的列表, 要使用/v0/topstories 入口. 获取每个 id的独立故事要使用/v0/items/<id> 入口.

v0/topstories 入口返回列表中 IDs的 400-500条故事. 因为我们要获取单个故事的数据,如果立刻获取500个故事的数据会严重影响性能. 为了解决这个问题,我们一次只获取20个故事的数据. 使用.slice()函数基于页面的故事 ID进行分割. 因为我们使用/v0/item/<id> 调用每个故事的数据, 因此使用Promise.all把所有的请求返回的promise对象压缩的一个数组中,然后用一个then(),resolve返回获取数据,并且保存 IDs 的顺序标记.

为了在应用管理我们的故事state,我们来创建一个story reducer. 创建src/store/story文件夹, 添加reducer.jsaction.js文件. 在action.js文件中添加代码

为 IDs请求和stor用的API 调用都创建了 request,success,failure的 actionTypes.

我们的actions 对象中包含了 用于请求管理的thunk 函数. 通过dispatch 函数而不是dispatch action 对象. 我们就可以在请求周期的不同点 dispatch 不同的acitons了.

函数getTopStoryIds会执行 API 调用,获取整个故事的列表. 在getTopStoryIds函数中success(成功)的回调函数执行时,我们会dispatch fetchStories action,用于获取第一页故事的结果.

当 API 调用成功返回时,就可以dispatch success Action,这样就可以使用新获取的数据来更新 Redux的 store了.

thunk软件包的基础实现只是用了几行代码. 要充分理解它,需要对 Redux的中间件有了解,但是从代码中,我们可以看到,如果我们使用一个函数来代替一个对象,就可以执行一个函数,并且把dispatch作为函数的参数传递.

现在我们需要创建reducer用于 Redux store中的数据存储. 在src/store/story/reudcer.js中添加代码

对于 FETCH_STORY_IDS_SUCCESS action type,我们展开当前 state和 payload. 在 payload 中唯一的键/值是storyIds,展开操作将会用新的值来更新 state.

对于FETCH_STORIES_SUCCESS action type. 在之前的故事列表中按顺序添加故事,以便于获取更多的页面. 此外,增加page 数, 设置isFetching state 为false.

现在,State已经由 Redux管理了, 我们就可以在组件中显示数据了.

把React APP 连接到 Redux Store

通过使用react-redux绑定,我们可以把组件连接到 Redux的store, 以props的形式接收Redux的 State.之后,只要 store 有更新,props就会引起组件的重新渲染,由此就更新了 UI.

在需要dispatch action 的组件中,以 props 的形式传递函数. 之后在组件内部调用这些函数,就可以触发 Redux store 中的state变化.

来看看如何在应用中管理这个变化. 返回到src/components/App文件夹,创建一个 App.js文件, 从src/components/App/index.js拷贝内容进来. 在index.js文件里面,我们将会把App组件连接到 Redux. 在index.js文件中添加代码

mapStateToProps函数接受 Redux store作为参数,返回一些属性到连接的组件中.对于App,我们需要 stories 数组, 当前页 page,storyIds数组还有isFetching指示器.

mapDispatchToProps函数接受dispatch函数作为参数,把返回的函数对象作为props传递给我们的组件. 创建的函数fetchStoriesFirstPage,执行时会dispatch action 来获取story IDs(然后获取第一页故事的内容).

我们在App.js中使用这两个props,首先添加componentDidMount,当组件在 DOM 中渲染完就可以立刻获取数据. 为List组件传递stories props.

src/components/List/index.js中,遍历stories 数组, 创建 ListItem组件的数组. 设置列表的key为story ID,并且展开story对象: ...story.展开操作会把对象的属性值作为单个的props传递给组件. key prop 是 React中组件作为数组加载时的一个策略,可以让列表形式的渲染更新速度更快.

如果现在观察屏幕,应该看到到的是硬编码的20行列表数据

我们需要使用从stories 获取的数据对ListItem进行更新.同时在 Hacker News中, 也会显示上次故事更新的时间和来源的地址. 需要安装
timeago.jsURl 软件包帮助计算没有通过 API 直接获取的数据, 使用下面命令执行安装

1
yarn add timeago.js url

需要编写助手函数来构建这些值. 从源码的src/utils文件夹中拷贝文件

现在更新 src/components/ListItem/index.js文件

通过这一步, 现在就可以在应用显示前20个故事了- cool!

使用无限滚动来对请求分页

现在,我们想实现的是当用于页面滚动到底部, 获取新的一页.回忆一下,每次成功获取故事之后,我们都增加了store中page的数字. 所以在第一页到达之后,Redux store 现在应该是page:1.我们需要在滚动到底部时dispatch fetchStories action.

为了实现无限滚动,我们会使用react-infinite-scroll-component组件. 我们也想实现一个方法来决定管是否要加载更多的页面,这一点我们使用reselect 在selector中实现.

1
yarn add react-infinite-scroll-component reselect

首先构建selector来计算是否有更多的故事存在. 创建 src/store/story/selecor.js文件. 为了判断是否有更多故事存在, 我们 Redux store中的storyIds 数组的长度是否和stories的长度相同, 如果stories的长度短一点,意思就是有更多的页面存在

src/components/App/index.js container中,导入hasMoreStoriesSelectormapStateToProps中添加 键hasMoreStories.同时在mapDispatchProps中添加fetchStories action,便于滚动时 dispatch action.

我们想在等待 API请求时使用动画显示. 创建src/components/Loader文件夹,index.jsstyles.js文件. 需要的动画是闪动的三个圆点.

styles.js文件中添加下面代码

@keyframe 是定义动画的 CSS 技术. 上面代码显示了在 Styled Components中的代码抽象. 有三个圆点,透明度从0.2开始增加到1, 然后返回到0.2, 给第二个和第三个点添加延迟,表现出弹跳式的偏移.

我们的Loader组件就是有三个独立span元素的动画styled components动画组件.

现在,准备为列表添加功能,在App组件中导入无限加载模块和Loader组件.也要创建fetchStories回调函数,将会调用fetchStories prop dispatch 下一页的action. 只有在isFetching为 false 时dispatch fetchStories action. 如果为 true.我们就多次获取统一页面. 你的src/components/App/App.js文件应该如下

当我们滚动到页面底部, 只要hasMoreStories为真,InfiniteScroll组件将会调用this.fetchStroies. 当fetchStories API 请求返回时,新的故事会添加到stories数组的尾部,渲染到页面中.

最后的挑战

在教程刚开始, 我们初始化了一个theme property.现在,留给你实现一个toggle功能. 在一些组件中添加点击事件,dispatch setTheme action.切换 lightdark的状态. 在ThemeProvider组件中需要一个三元条件判断如果 state.app.theme==='dark'就传递colorDark,否则就传递colorsLight.

如果你卡住了,可以看看源码的实现.加入Slack 寻求帮助. 试试我们的办法.

部署到GitHub 主页

对于应用的最后一步都是投入生产. 因为我们的功能是在客户端,可以免费部署在 GitHub 主页的静态网站.

提交你的代码并推送到Github. 我命名仓库为hn-clone.如果你在创建仓库和上传代码是遇到问题可以参照一下这个指导

现在使用如下的步骤发送过的 GitHub 主页:

  1. 在package.json文件中添加 "homepage":"http://<username>.github.io/<repo-name>" 使用你的实际值替换<username><repo-name>. 我的值是 treyhuffinehn-clone.

  1. 安装gh-pages作为开发依赖项
1
yarn add -D gh-pages
  1. package.json文件中添加两个脚本
1
"predeploy": "npm run build","deploy": "gh-pages -d build"

  1. 最后运行yarn deploy 并访问在homepage中定义的URL.

现在你的 Hacker News 投入生产了.

结论

本文覆盖了构建 Hacker News clone所必须的所有的功能. 源码还有一些额外的特性,持续更新中, 查看一下是否有灵感出现可以继续添加功能,学习更多的 React 知识.

不要忘了下载Chrome 扩展, 并访问 gitconnectec.com网站,加入开发者社区.

原文发表在 gitconnected.com-开发者社区